/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * ClassCache.java
 * Copyright (C) 2010-2014 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.io.File;
import java.io.FileFilter;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * A singleton that stores all classes on the classpath.
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class ClassCache {

    /**
     * For filtering classes.
     * 
     * @author fracpete (fracpete at waikato dot ac dot nz)
     * @version $Revision$
     */
    public static class ClassFileFilter implements FileFilter {

        /**
         * Checks whether the file is a class.
         * 
         * @param pathname the file to check
         * @return true if a class file
         */
        @Override
        public boolean accept(File pathname) {
            return pathname.getName().endsWith(".class");
        }
    }

    /**
     * For filtering classes.
     * 
     * @author fracpete (fracpete at waikato dot ac dot nz)
     * @version $Revision$
     */
    public static class DirectoryFilter implements FileFilter {

        /**
         * Checks whether the file is a directory.
         * 
         * @param pathname the file to check
         * @return true if a directory
         */
        @Override
        public boolean accept(File pathname) {
            return pathname.isDirectory();
        }
    }

    /** whether to output some debug information. */
    public final static boolean VERBOSE = false;

    /** the key for the default package. */
    public final static String DEFAULT_PACKAGE = "DEFAULT";

    /**
     * for caching all classes on the class path (package-name &lt;-&gt; HashSet
     * with classnames).
     */
    protected Hashtable<String, HashSet<String>> m_Cache;

    static {
        // notify if VERBOSE is still on
        if (VERBOSE) {
            System.err.println(ClassCache.class.getName() + ": VERBOSE ON");
        }
    }

    /**
     * Initializes the cache.
     */
    public ClassCache() {
        super();
        initializeNew();
    }

    /**
     * Fixes the classname, turns "/" and "\" into "." and removes ".class".
     * 
     * @param classname the classname to process
     * @return the processed classname
     */
    public static String cleanUp(String classname) {
        String result;

        result = classname;

        if (result.indexOf("/") > -1) {
            result = result.replace("/", ".");
        }
        if (result.indexOf("\\") > -1) {
            result = result.replace("\\", ".");
        }
        if (result.endsWith(".class")) {
            result = result.substring(0, result.length() - 6);
        }

        return result;
    }

    /**
     * Extracts the package name from the (clean) classname.
     * 
     * @param classname the classname to extract the package from
     * @return the package name
     */
    public static String extractPackage(String classname) {
        if (classname.indexOf(".") > -1) {
            return classname.substring(0, classname.lastIndexOf("."));
        } else {
            return DEFAULT_PACKAGE;
        }
    }

    /**
     * Adds the classname to the cache.
     * 
     * @param classname the classname, automatically removes ".class" and turns "/"
     *                  or "\" into "."
     * @return true if adding changed the cache
     */
    public boolean add(String classname) {
        String pkgname;
        HashSet<String> names;

        // classname and package
        classname = cleanUp(classname);
        pkgname = extractPackage(classname);

        // add to cache
        if (!m_Cache.containsKey(pkgname)) {
            m_Cache.put(pkgname, new HashSet<String>());
        }
        names = m_Cache.get(pkgname);
        return names.add(classname);
    }

    /**
     * Removes the classname from the cache.
     * 
     * @param classname the classname to remove
     * @return true if the removal changed the cache
     */
    public boolean remove(String classname) {
        String pkgname;
        HashSet<String> names;

        classname = cleanUp(classname);
        pkgname = extractPackage(classname);
        names = m_Cache.get(pkgname);
        if (names != null) {
            return names.remove(classname);
        } else {
            return false;
        }
    }

    /**
     * Fills the class cache with classes in the specified directory.
     * 
     * @param prefix the package prefix so far, null for default package
     * @param dir    the directory to search
     */
    protected void initFromDir(String prefix, File dir) {
        File[] files;

        // check classes
        files = dir.listFiles(new ClassFileFilter());
        if (files != null) {
            for (File file : files) {
                if (prefix == null) {
                    add(file.getName());
                } else {
                    add(prefix + "." + file.getName());
                }
            }
        }

        // descend in directories
        files = dir.listFiles(new DirectoryFilter());
        if (files != null) {
            for (File file : files) {
                if (prefix == null) {
                    initFromDir(file.getName(), file);
                } else {
                    initFromDir(prefix + "." + file.getName(), file);
                }
            }
        }
    }

    /**
     * Fills the class cache with classes in the specified directory.
     * 
     * @param dir the directory to search
     */
    protected void initFromDir(File dir) {
        if (VERBOSE) {
            System.out.println("Analyzing directory: " + dir);
        }
        initFromDir(null, dir);
    }

    /**
     * Analyzes the MANIFEST.MF file of a jar whether additional jars are listed in
     * the "Class-Path" key.
     * 
     * @param manifest the manifest to analyze
     */
    protected void initFromManifest(Manifest manifest) {
        if (manifest == null) {
            return;
        }

        Attributes atts;
        String cp;
        String[] parts;

        atts = manifest.getMainAttributes();
        cp = atts.getValue("Class-Path");
        if (cp == null) {
            return;
        }

        parts = cp.split(" ");
        for (String part : parts) {
            if (part.trim().length() == 0) {
                return;
            }
            if (part.toLowerCase().endsWith(".jar")) {
                initFromClasspathPart(part);
            }
        }
    }

    /**
     * Fills the class cache with classes from the specified jar.
     * 
     * @param file the jar to inspect
     */
    protected void initFromJar(File file) {
        JarFile jar;
        JarEntry entry;
        Enumeration<JarEntry> enm;

        if (VERBOSE) {
            System.out.println("Analyzing jar: " + file);
        }

        if (!file.exists()) {
            System.out.println("Jar does not exist: " + file);
            return;
        }

        try {
            jar = new JarFile(file);
            enm = jar.entries();
            while (enm.hasMoreElements()) {
                entry = enm.nextElement();
                if (entry.getName().endsWith(".class")) {
                    add(entry.getName());
                }
            }
            initFromManifest(jar.getManifest());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Returns all the stored packages.
     * 
     * @return the package names
     */
    public Enumeration<String> packages() {
        return m_Cache.keys();
    }

    /**
     * Returns all the classes for the given package.
     * 
     * @param pkgname the package to get the classes for
     * @return the classes (sorted by name)
     */
    public HashSet<String> getClassnames(String pkgname) {
        if (m_Cache.containsKey(pkgname)) {
            return m_Cache.get(pkgname);
        } else {
            return new HashSet<String>();
        }
    }

    /**
     * Analyzes a part of the classpath.
     * 
     * @param part the part to analyze
     */
    protected void initFromClasspathPart(String part) {
        File file;

        file = null;
        if (part.startsWith("file:")) {
            part = part.replace(" ", "%20");
            try {
                file = new File(new java.net.URI(part));
            } catch (URISyntaxException e) {
                System.err.println("Failed to generate URI: " + part);
                e.printStackTrace();
            }
        } else {
            file = new File(part);
        }
        if (file == null) {
            System.err.println("Skipping: " + part);
            return;
        }

        // find classes
        if (file.isDirectory()) {
            initFromDir(file);
        } else if (file.exists()) {
            initFromJar(file);
        }
    }

    /**
     * Initializes the cache.
     */
    protected void initialize() {
        String part = "";
        URLClassLoader sysLoader;
        URL[] urls;

        m_Cache = new Hashtable<String, HashSet<String>>();

        sysLoader = (URLClassLoader) getClass().getClassLoader();
        urls = sysLoader.getURLs();
        for (URL url : urls) {
            part = url.toString();
            if (VERBOSE) {
                System.out.println("Classpath-part: " + part);
            }
            initFromClasspathPart(part);
        }
    }

    protected void initializeNew() {
        m_Cache = new Hashtable<String, HashSet<String>>();
        WekaPackageClassLoaderManager wcl = WekaPackageClassLoaderManager.getWekaPackageClassLoaderManager();

        // parent classloader entries...
        URL[] sysOrWekaCP = wcl.getWekaClassloaderClasspathEntries();
        for (URL url : sysOrWekaCP) {
            String part = url.toString();
            if (VERBOSE) {
                System.out.println("Classpath-part: " + part);
            }
            initFromClasspathPart(part);
        }

        // top-level package jar file class entries
        Set<String> classes = wcl.getPackageJarFileClasses();
        for (String cl : classes) {
            add(cl);
        }
    }

    /**
     * Find all classes that have the supplied matchText String in their suffix.
     * 
     * @param matchText the text to match
     * @return an array list of matching fully qualified class names.
     */
    public ArrayList<String> find(String matchText) {
        ArrayList<String> result;
        Enumeration<String> packages;
        Iterator<String> names;
        String name;

        result = new ArrayList<String>();

        packages = m_Cache.keys();
        while (packages.hasMoreElements()) {
            names = m_Cache.get(packages.nextElement()).iterator();
            while (names.hasNext()) {
                name = names.next();
                if (name.contains(matchText)) {
                    result.add(name);
                }
            }
        }

        if (result.size() > 1) {
            Collections.sort(result);
        }

        return result;
    }

    /**
     * For testing only.
     * 
     * @param args ignored
     */
    public static void main(String[] args) {
        ClassCache cache = new ClassCache();
        Enumeration<String> packages = cache.packages();
        while (packages.hasMoreElements()) {
            String key = packages.nextElement();
            System.out.println(key + ": " + cache.getClassnames(key).size());
        }
    }
}
